Skip to content

feat(denario): onboard Denario — partner wallet, Gold/Silver Polygon assets, and permanent referral alias#4209

Open
joshuakrueger-dfx wants to merge 4 commits into
DFXswiss:developfrom
joshuakrueger-dfx:agent/add-denario-permanent-ref
Open

feat(denario): onboard Denario — partner wallet, Gold/Silver Polygon assets, and permanent referral alias#4209
joshuakrueger-dfx wants to merge 4 commits into
DFXswiss:developfrom
joshuakrueger-dfx:agent/add-denario-permanent-ref

Conversation

@joshuakrueger-dfx

@joshuakrueger-dfx joshuakrueger-dfx commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Overview

Bundles the full Denario onboarding into one PR:

  1. Partner wallet — add Denario to the wallet table (analogous to existing partner wallets, e.g. Cake Wallet).
  2. Assets — add the two Denario precious-metal tokens on Polygon (DGC, DSC) as inert, list-only assets.
  3. Referral alias — permanent denario alias on the ref-keys setting (original scope of this PR).

All of it ships via a single reversible, idempotent migration each, plus the seed mirror for local dev.


1. Partner wallet

New migration 1784038000000-AddDenarioWalletAndAssets.js inserts a Denario row into wallet. Only name/displayName are set; every compliance/behaviour column takes its conservative DB default (isKycClient=false, autoTradeApproval=false, usesDummyAddresses=false, displayFraudWarning=false, amlRules='0', buySpecificIbanEnabled=false) — kept conservative on purpose so the partner cannot bypass any check by default. Idempotent on name (not a unique column), reversible.

Needs DFX confirmation: the partner's actual KYC/AML setup (whether isKycClient / amlRules should differ from the safe defaults). Adjust in a follow-up if required.

2. Assets (Polygon ERC-20)

Added in the same migration and mirrored into migration/seed/asset.csv (ids 411/412), values verified on-chain (decimals()/symbol()/name()):

Name Symbol Contract Decimals
Denario Gold Coin DGC 0xf7e2…042f 8
Denario Silver Coin DSC 0x5d4e…c7e7 8

Contract addresses independently verified against Denario's official token pages (DGC 0xf7E2…042f, DSC 0x5D4E…c7E7). Backing: 1 DGC = 1 oz gold, 1 DSC = 1 oz silver (999.9), vaulted in Switzerland.

Both are inert, list-only (same pattern as OlkyFrozen/EUR): type=Token, blockchain=Polygon, category=Public, no priceRuleId, and every trade/payment flag falseisActive=false. Consequences:

  • Excluded from the hourly price job (no priceRuleId).
  • Not buyable/sellable/payable — no cron/observable/liquidity path picks them up.
  • No automatic liquidity-management mechanism to buy or sell these tokens; any purchase or sale is handled manually, as required.
  • financialType is intentionally left null (no precious-metal type exists yet) — set it when trading is enabled.

Reference txs: DGC · DSC

Note (not a liquidity mechanism): once the assets exist, the Polygon payin strategy will recognise inbound DGC/DSC transfers to DFX deposit addresses and map them to the correct asset with 8 decimals — identical to every other listed Polygon token (e.g. the already-listed non-tradeable XCHF/USDC.e). This is deposit detection/forwarding, not an automatic buy/sell.

Deliberately left empty (correct for an inert asset — not guessed)

  • financialType: no precious-metal type exists in the repo (values in use: Other/USD/EUR/BTC/CHF/DEPS/FPS, plus 2 empty). It drives fee-matching (fee.entity) and balance grouping (log-job) — introducing a new bucket is a tax/reporting classification decision for DFX/DAS, so left null.
  • approxPriceUsd/Chf/Eur: would hard-code a decaying gold/silver snapshot; these fields are only read for active/custody/traded assets, so an inert asset never uses them.
  • sortOrder: display-order only (e.g. dEURO/SAND also leave it empty).

Follow-ups when trading is enabled (verified inputs, need DFX/DAS decision)

  • Pricing is available (not on CoinGecko, but): DIA on-chain price feed for DSC, and a QuickSwap DSC/USDC pool on Polygon (0x6722e2405a3b6ee5bf862112a143cf7228ee6c18, live ≈ $36.55 ≈ 1 oz silver). Denario also publishes Proof-of-Reserve oracles (DSC PoR 0xb507a787cda02d6086f101d6067e9c35d58f1fc5). Attach a PriceRule (DEX/oracle source) then flip the buy/sell flags.
  • Set financialType per token once the metal asset class is decided.
  • Confirm partner-wallet compliance fields (isKycClient/amlRules).
  • Out of scope but available: DGC/DSC also exist on Plume, Soneium and BNB (BinanceSmartChain) — this PR lists Polygon only, as requested.

3. Referral alias (original scope)

  • add a permanent denario alias to the existing ref-keys setting
  • resolve the target referral code from the active Denario organization account in each database, so production and development can use their own environment-local codes
  • preserve all existing aliases and make the migration reversible and idempotent
  • fail closed on a missing, inactive, ambiguous, malformed, or conflicting target/configuration
  • fix /v1/app?code=<alias> to persist the resolved referral code instead of the unresolved alias, matching /v1/app/:app

Stable links after deployment

  • Production: https://api.dfx.swiss/v1/app/services?code=denario
  • Development: https://dev.api.dfx.swiss/v1/app/services?code=denario
  • Generic redirect: /v1/app?code=denario

Deployment precondition

Each environment must contain exactly one active organization account whose trimmed, case-insensitive organization name is Denario or Denario AG, with exactly one active user that already has a valid referral code. The migration intentionally aborts if this invariant is not met. It also refuses to overwrite an existing denario alias that points elsewhere.


Tests & checks

Wallet + assets migration

  • reviewed twice (conformance + logic) → 0 findings; on-chain contract/decimals independently confirmed via Polygon RPC
  • idempotent (NOT EXISTS guards on uniqueName / name) and reversible (down() removes both assets + the wallet)
  • seed rows: 29 columns, no id collision (prev max 410), consistent with the migration
  • Prettier + Node syntax checks pass

Referral alias

  • controller coverage for both redirect routes, direct refs, unknown aliases, and origin-only tracking
  • migration coverage for preservation, creation, idempotency, conflicts, invalid/missing/ambiguous targets, malformed settings, and rollback
  • the real migration SQL is executed against pg-mem to validate PostgreSQL semantics and reversible state changes
  • full Jest suite: 189 suites passed, 3,012 tests passed, 140 tests skipped by their existing environment guards
  • npm run type-check, npm run lint, npm run format:check, npm run build

@joshuakrueger-dfx joshuakrueger-dfx changed the title feat(referral): add permanent Denario referral alias feat(denario): onboard Denario — partner wallet, Gold/Silver Polygon assets, and permanent referral alias Jul 15, 2026
@joshuakrueger-dfx

Copy link
Copy Markdown
Collaborator Author

Completed a full-diff conformance and logic review (1 round, 0 findings) before marking ready.

Verified:

  • Wallet + asset migration is idempotent (NOT EXISTS guards on name/uniqueName) and reversible; mirrors the merged AddOlkyFrozenAsset precedent (migration + loc-only seed row).
  • DGC/DSC contract addresses and decimals (8) confirmed on-chain and against Denario's official token pages; 1 DGC = 1 oz gold, 1 DSC = 1 oz silver.
  • Assets are inert/list-only (no priceRuleId, all trade flags false → isActive=false): excluded from the price job and every buy/sell/liquidity path — no automatic liquidity mechanism, manual only, as intended.
  • Partner wallet added with conservative DB defaults (compliance fields flagged for confirmation); not mirrored into the wallet seed (partners are prod-only).
  • Referral alias unchanged and still fail-closed/reversible.
  • All CI checks green.

@joshuakrueger-dfx joshuakrueger-dfx marked this pull request as ready for review July 15, 2026 09:32

@TaprootFreak TaprootFreak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found three blocking correctness/data-safety issues. The asset rows currently activate pay-in detection without providing a processable pricing/manual path, and both migrations can delete or overwrite state they did not create. Please address the inline findings before merge.

("name", "uniqueName", "type", "blockchain", "category", "dexName", "chainId", "decimals", "description",
"buyable", "sellable", "cardBuyable", "cardSellable", "instantBuyable", "instantSellable",
"paymentEnabled", "refEnabled", "refundEnabled", "ikna", "personalIbanEnabled", "comingSoon")
SELECT 'DGC', 'Polygon/DGC', 'Token', 'Polygon', 'Public', 'DGC', '0xf7e2d612f1a0ce09ce9fc6fc0b59c7fd5b75042f', 8, 'Denario Gold Coin',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Prevent inert DGC/DSC deposits from getting stuck in the pay-in pipeline. Supplying chainId makes Alchemy map inbound Polygon transfers to these assets because the register strategy loads all blockchain assets, regardless of isActive. The registration flow then calls validateInput(), which requires an asset→CHF price; with no priceRuleId, pricing throws and the catch leaves the pay-in in CREATED, so the minute job retries and logs it indefinitely. This contradicts the PR's claim that recognition/forwarding is harmless. Either provide the complete priced/manual processing path or explicitly exclude these inert assets from pay-in registration, and add a DGC/DSC deposit regression test.

const value = JSON.stringify(refKeys);

if (row) {
await queryRunner.query(`UPDATE "setting" SET "value" = $1, "updated" = NOW() WHERE "key" = $2`, [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve and audit the previous ref-keys state before overwriting it. Production already has this setting, so this replaces the serialized value without a durable before→after record, contrary to CONTRIBUTING.md's critical “Auditable mutations — no destructive overwrites” rule. Rollback ownership is also unsafe: if denario already pointed to the same ref, up() returns without changing anything, but down() still deletes that pre-existing alias; it also deletes a value changed after deployment. Persist an immutable prior-state/audit record before the update (fail closed if it cannot be written) and make down() restore only the change actually owned by this migration.

* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`DELETE FROM "asset" WHERE "uniqueName" IN ('Polygon/DGC', 'Polygon/DSC')`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not delete rows that up() may not have created. Each up() insert is guarded by NOT EXISTS, but down() unconditionally deletes every matching asset and wallet (and wallet.name is not unique). In a pre-seeded environment or after migration-history recovery, an up→down cycle therefore removes pre-existing data. Either fail closed when matching rows already exist or persist exact migration ownership/prior state and revert only that state; add an up→down test covering the pre-existing-row case.

- pay-in register strategies recognize only priced assets (new AssetService.getPayInAssets); an unpriced token is no longer mapped into the register flow, where validateInput/pricing throws and would loop the pay-in in CREATED forever
- AddDenarioPermanentRef: persist an immutable before-image of ref-keys before overwriting (fail closed in the migration transaction) and revert only the alias this migration set
- AddDenarioWalletAndAssets: down() removes only still-inert assets and the wallet only while it has no owner or users
- specs: unpriced-asset exclusion from recognition, ref-keys audit/ownership, wallet/asset up->down and ownership preservation
@joshuakrueger-dfx

Copy link
Copy Markdown
Collaborator Author

Thanks — addressed all three findings in 5f4f986.

1. Inert DGC/DSC deposits stuck in the pay-in pipeline. Pay-in register strategies now recognize only priced assets via a new AssetService.getPayInAssets (SQL filter priceRule IS NOT NULL, scoped to the pay-in path with its own cache key, so balances/monitoring/asset-API are untouched). An unpriced token now maps to asset = nullCryptoInput is created as FAILED → never re-selected by getNewPayIns, so no CREATED retry loop and no per-minute logging. Applied to all register strategies (not just Alchemy), since the same map-by-chainId-then-price pattern was latent on every chain (e.g. Ethereum/ONDO). Regression test added: an unpriced token is excluded from recognition.

2. ref-keys overwrite without a durable before→after record. AddDenarioPermanentRef now writes an immutable before-image (ref-keys.backup.<migration-id> = { existed, previous, ownedRef }) before the update; the migration is transactional, so a failed backup insert rolls back the ref-keys change (fail closed). down() restores from the backup and removes the alias only while it still carries the value this migration set — an admin re-point or a later-added alias is preserved.

3. down() deleting rows it may not have created. AddDenarioWalletAndAssets.down() now removes the assets only while still inert (priceRuleId IS NULL AND NOT buyable AND NOT sellable) and the wallet only while ownerId IS NULL and no user references it. Anything activated or linked afterwards is left in place (roll-forward). Added an up→down migration spec covering the pre-existing/activated-row case.

All local gates green (type-check, lint, 23 tests across the 3 new/updated specs).

Copy link
Copy Markdown
Collaborator

Follow-up review of 5f4f986: the targeted fixes improve the PR, but two blocking issues remain, plus one broader pay-in regression risk.

  1. [P1] AddDenarioWalletAndAssets.down() still deletes rows that up() may not have created.
    The new predicates describe the rows' current state; they do not establish migration ownership. If inert DGC/DSC rows or an unused Denario wallet already exist, up() skips them via NOT EXISTS, but down() still deletes them. The wallet query can also delete multiple rows because wallet.name is not unique. Additionally, assets activated only through cardBuyable, cardSellable, instantBuyable, instantSellable, paymentEnabled, or refEnabled still match the delete. Please either fail closed on pre-existing rows or persist the exact IDs created by this migration. The migration spec should pre-insert the rows before up() and verify that up() -> down() preserves them; the current test only activates rows created by up().

  2. [P1] The supposedly immutable ref-keys audit record is destroyed by down().
    Writing the before-image before the snapshot update fixes the forward path, but down() unconditionally deletes ref-keys.backup.1784037000000 after updating—or deciding not to update—the snapshot. After rollback, the database can no longer reconstruct which alias value was removed, the before→after transition, or whether rollback skipped the change because an admin had re-pointed it. This still conflicts with CONTRIBUTING's critical append-only/reconstructible mutation rule. Keep the audit row as superseded/rolled back, or write a separate immutable rollback event before changing the snapshot; do not delete the only history row.

  3. [P2] Guard the global pay-in filter against active assets without a linked price rule.
    Switching every register strategy to getPayInAssets() stops the unpriced-token retry loop, but the fresh seed already contains an active counterexample: Ethereum/ONDO is buyable/sellable while its priceRuleId is empty, even though price rule 60 exists. A legitimate ONDO sell deposit is therefore mapped to an unknown asset and saved as FAILED. Please link ONDO to its rule and add a seed/config invariant asserting that every active pay-in/sell asset is priced. The current AssetService test only verifies the mocked list/filter; it does not exercise a register strategy, the resulting CryptoInput status, or the no-retry behavior.

The referral controller change itself looks correct, and the DGC/DSC retry-loop cause is addressed. I reran the four targeted suites locally: 29 tests passed. The remaining blockers are test gaps and ownership/audit semantics, not failing CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants